#!/usr/bin/env python2.6
__version__ = '2.0.0-a'


class RuleError (Exception):
	pass


class Argument (object):
	def __init__ (self, Value, Invert=False):
		self.Value = Value
		self.Invert = bool (Invert)

	def __str__ (self):
		Tpl = 'not "%s"' if self.Invert else '"%s"'
		return Tpl % self.Value


class IPv4 (Argument):
	def __init__ (self, Argument):
		from socket import inet_aton
		self.MASKS = [inet_aton (Mask) for Mask in (
			'0.0.0.0',
			'128.000.000.000', '192.000.000.000', '224.000.000.000', '240.000.000.000',
			'248.000.000.000', '252.000.000.000', '254.000.000.000', '255.000.000.000',
			'255.128.000.000', '255.192.000.000', '255.224.000.000', '255.240.000.000',
			'255.248.000.000', '255.252.000.000', '255.254.000.000', '255.255.000.000',
			'255.255.128.000', '255.255.192.000', '255.255.224.000', '255.255.240.000',
			'255.255.248.000', '255.255.252.000', '255.255.254.000', '255.255.255.000',
			'255.255.255.128', '255.255.255.192', '255.255.255.224', '255.255.255.240',
			'255.255.255.248', '255.255.255.252', '255.255.255.254', '255.255.255.255',
		)]
		Addr = Argument.Value
		Type = 'hostname'
		for Func in (self.any, self.same, self.self, self.single, self.network, self.range):
			Test = Func (Addr)
			if Test:
				Addr = Test
				Type = Func.__name__
				break
		super (IPv4, self).__init__ (Addr, Argument.Invert)
		self.Type = Type

#	def __str__ (self):
#		Tpl = '(%s) not "%s"' if self.Invert else '(%s) "%s"'
#		return Tpl % (self.Type, self.Value)

	def any (self, IPv4):
		return IPv4 if IPv4 == '(any)' else False

	def same (self, IPv4):
		return IPv4 if IPv4 == '(same)' else False

	def self (self, IPv4):
		return IPv4 if IPv4 == '(self)' else False

	def single (self, IPv4):
		from socket import inet_aton, inet_ntoa, error as SocketError
		try:
			Addr = inet_aton (IPv4)
		except SocketError as X:
			return False
		else:
			return inet_ntoa (Addr)

	def network (self, IPv4):
		from socket import inet_aton
		Network = IPv4.split ('/', 1)
		if len (Network) < 2:
			return False
		Address, Prefix = Network
		Address = self.single (Address)
		if Address:
			if Prefix.isdigit ():
				Prefix = int (Prefix)
				if Prefix not in xrange (33):
					raise RuleError ("Network address '%s' has invalid prefix '%d'" % IPv4, Prefix)
			else:
				if self.single (Prefix):
					Prefix = inet_aton (Prefix)
					if Prefix not in self.NETMASKS:
						raise RuleError ("Network address '%s' has invalid netmask '%s'" % IPv4, Prefix)
				else:
					raise RuleError ("Network address '%s' has invalid netmask '%s'" % IPv4, Prefix)
		else:
			raise RuleError ("Network address '%s' is invalid" % IPv4)
		return "%s/%d" % (Address, Prefix)

	def range (self, IPv4):
		Range = IPv4.split ('-', 1)
		if len (Range) < 2:
			return False
		Floor, Ceil = Range
		Floor = self.single (Floor)
		if not Floor:
			raise RuleError ("Address range '%s' starts with an invalid address" % IPv4)
		Ceil = self.single (Ceil)
		if not Ceil:
			raise RuleError ("Address range '%s' ends with an invalid address" % IPv4)
		return "%s-%s" % (Floor, Ceil)


class Ports (Argument):
	def __init__ (self, Argument):
		from socket import getservbyname, error as SocketError
		Portlist = []
		for Range in Argument.Value.split (','):
			Range = Range.strip ()
			Range = Range.split ('-', 1)
			if len (Range) < 2:
				Port, = Range
				if not Port.isdigit ():
					try:
						Port = str (getservbyname (Port.lower ()))
					except SocketError as X:
						raise RuleError ("Invalid port '%s': %s" % (Port, X))
				Portlist.append (Port)
			else:
				for Port in Range:
					if not Port.isdigit ():
						raise RuleError ("Invalid port '%s': ports ranges must be numbers" % Port)
				Floor, Ceil = Range
				if Floor >= Ceil:
					raise RuleError ("Invalid port range '%s-%s': second port must be greater than first" % (Floor, Ceil))
				Portlist.append (':'.join (Range))
		super (Ports, self).__init__ (','.join (Portlist), Argument.Invert)


class Rewrite (object):
	def __init__ (self, Arg):
		Value = Arg.Value.split (':', 1)
		if len (Value) < 2:
			Addr, = Value
			Ports = None
		else:
			Addr, Ports = Value
		self.IPv4 = IPv4 (Argument (Addr))
		self.Ports = Ports

	def __str__ (self):
		return "%s:%s" % (self.IPv4, self.Ports)


class Tokens (list):
	def __init__ (self, Line):
		from shlex import split
		try:
			T = split (Line, False, True)
		except ValueError as X:
			raise RuleError (str (X))
		super (Tokens, self).__init__ (reversed (T))
		self.Raw = None
		self.Current = None
		self.Last = None

	def __call__ (self):
		if not self:
			return False
		self.Last = self.Current
		self.Raw = self.pop ()
		self.Current = self.Raw.lower ()
		return True

	def argument (self, InvertAllowed=True):
		if not self:
			raise RuleError ("Unexpected end of line, was expecting argument")
		Invert = False
		Value = self.pop ()
		if Value.lower () == 'not':
			if InvertAllowed:
				if not self:
					raise RuleError ("Unexpected end of line after 'not', was expecting argument")
				Invert = True
				Value = self.pop ()
			else:
				raise RuleError ("Unexpected 'not', was expecting argument")
		return Argument (Value, Invert)


class Rule (object):
	def __init__ (self, RuleDef):
		self.Origin = RuleDef
		self.R = {}
		self.SIMPLE_ACTIONS = ('pass', 'drop', 'reject',)
		self.REWRITE = ('rewrite-to',)
		self.SWITCHES = ('random',)
		self.DIRECTIONS = ('in', 'out',)
		self.ADDRESS_TYPES = ('from', 'to',)
		self.PROTOCOLS = ('icmp', 'tcp', 'udp',)
		self.STATES = ('new', 'established', 'related', 'invalid',)
		self.FINALS = ('options', '#',)
		T = Tokens (RuleDef)
		while T ():
			if T.Current in self.SIMPLE_ACTIONS:
				self.set_once ('action', T.Current, "Multiple actions are not allowed")
			elif T.Current in self.REWRITE:
				self.set_once ('action', T.Current, "Multiple actions are not allowed")
				self.set_once (T.Current, Rewrite (T.argument (False)), None)
			elif T.Current in self.SWITCHES:
				self.set_once (T.Current, True, "Multiple '%s' instructions are not allowed" % T.Current)
			elif T.Current in self.DIRECTIONS:
				Dir = self.R.get ('dir')
				if Dir is None:
					self.R ['dir'] = T.Current
				elif Dir == 'fwd':
					self.R ['dir'] = 'invalid'
				else:
					self.R ['dir'] = 'invalid' if Dir == T.Current else 'fwd'
				if self.R ['dir'] == 'invalid':
					raise RuleError ("Multiple '%s' instructions are not allowed" % T.Current)
			elif T.Current == 'on':
				if T.Last in self.DIRECTIONS:
					self.R [T.Last] = T.argument ()
				else:
					raise RuleError ("'on' instruction must follow 'in' or 'out' instructions")
			elif T.Current in self.ADDRESS_TYPES:
				self.set_once (T.Current, IPv4 (T.argument ()), "Multiple '%s' instructions are not allowed" % T.Current)
			elif T.Current == 'ports':
				if T.Last in self.ADDRESS_TYPES:
					self.R.setdefault ('ports', {})
					self.R ['ports'][T.Last] = Ports (T.argument ())
				else:
					raise RuleError ("'ports' instruction is only allowed after 'from' and 'to' instructions")
			elif T.Current in self.PROTOCOLS:
				self.set_once ('proto', Argument (T.Current), "Multiple protocol definitions are not allowed")
			elif T.Current == 'proto':
				self.set_once ('proto', T.argument (), "Multiple protocol definitions are not allowed")
			elif T.Current in self.STATES:
				self.set_once ('state', T.Current, "Multiple state definitions are not allowed")
			elif T.Current in self.FINALS:
				self.R [T.Current] = list (reversed (T))
				del T [:]
			else:
				raise RuleError ("Unknown instruction '%s'" % T.Raw)
#		print self.R

	def set_once (self, Key, Value, ErrStr):
		if self.R.get (Key) is not None:
			raise RuleError (ErrStr)
		self.R [Key] = Value

#	def PortsWithoutRightProto (self):
#		PortsDefined = False
#		if self.From and self.From.get ('Ports'):
#			PortsDefined = True
#		if self.To and self.To.get ('Ports'):
#			PortsDefined = True
#		if self.Rewrite and self.Rewrite.get ('Ports'):
#			PortsDefined = True
#		Proto_TCP_or_UDP = self.Proto and self.Proto ['Value'].lower () in ['tcp', 'udp', '6', '17'] and not self.Proto ['Except']
#		return PortsDefined and not Proto_TCP_or_UDP

#	def CheckPorts (self, Address, NAT=False):
#		from re import split
#		if Address:
#			Ports = Address.get ('Ports')
#			if Ports:
#				Ports = Ports ['Value']
#				if Ports.isdigit ():
#					Ports = [Ports]
#				else:
#					if NAT:
#						Ports = Ports.partition ('-')
#						if Ports [0].isdigit () and Ports [1] == '-' and Ports [2].isdigit ():
#							Ports = [Ports [0], Ports [2]]
#						else:
#							self.PortsError = 'syntax: <port> or <port>-<port> (inclusive range)'
#							return False
#					else:
#						Ports = split ('[,:]', Ports)
#						if len (Ports) > 15:
#							self.PortsError = '15 numbers or less please'
#							return False
#						if not reduce (lambda Ax, Test: Test.isdigit () if Ax else False, Ports, True):
#							self.PortsError = 'check the syntax; for reference man iptables (search for multiport)'
#							return False
#				if not reduce (lambda Ax, Test: int (Test) >= 0 and int (Test) <= 65535 if Ax else False, Ports, True):
#					self.PortsError = 'port must be an integer from 0 to 65535 inclusive'
#					return False
#		return True

#	def Check (self):
#		if self.Default:
#			if self.Action != 'pass' and self.Action != 'drop':
#				raise RuleError ("Only 'pass' and 'drop' instructions can be default")
#			if self.In:
#				pass
#		if self.Action != 'rewrite' and self.Random:
#			raise RuleError ("'random' instruction can only be used with 'rewrite' action")
#		if self.Action == 'rewrite' and self.In and self.Out:
#			raise RuleError ("'rewrite' instruction does not make sense with both 'in' and 'out'")
#		if self.Action != 'rewrite' and self.Rewrite:
#			raise RuleError ("'->' instruction does only make sense with 'rewrite' action")
#		if not self.CheckPorts (self.From):
#			raise RuleError ("Bad ports definition in 'from' instruction: " + self.PortsError)
#		if not self.CheckPorts (self.To):
#			raise RuleError ("Bad ports definition in 'to' instruction: " + self.PortsError)
#		if not self.CheckPorts (self.Rewrite, True):
#			raise RuleError ("Bad ports definition in '->' instruction: " + self.PortsError)
#		if not self.Options:
#			if not self.Action:
#				raise RuleError ("Please specify an action")
#			if self.Action == 'rewrite' and self.Rewrite:
#				raise RuleError ("'rewrite' instruction expects '->' instruction as well")
#			if self.PortsWithoutRightProto ():
#				raise RuleError ("Ports can only be specified with TCP or UDP protocols")

	def __str__ (self):
		Result = []
		s = self.R.get ('action')
		if s:
			Result.append (s)
		if s in self.REWRITE:
			s = self.R.get ('rewrite-to')
			Result.append (str (s))
		s = self.R.get ('state')
		if s:
			Result.append (s)
		for i in self.SWITCHES:
			s = self.R.get (i)
			if s:
				Result.append (i)
		s = self.R.get ('dir')
		if s == 'fwd':
			for i in self.DIRECTIONS:
				Result.append (i)
				s = self.R.get (i)
				if s:
					Result += (('on', str (self.R.get (i))))
		elif s in self.DIRECTIONS:
			Result.append (s)
			s = self.R.get (s)
			if s:
				Result += (('on', str (s)))
		for i in self.ADDRESS_TYPES:
			s = self.R.get (i)
			if s:
				Result += ((i, str (s)))
				s = self.R.get ('ports')
				if s:
					s = self.R ['ports'].get (i)
					if s:
						Result += (('ports', str (s)))
		s = self.R.get ('proto')
		if s:
			Result += ('proto', str (s))
		for i in self.FINALS:
			s = self.R.get (i)
			if s:
				Result += (i,) + tuple ('"%s"' % Arg for Arg in s)
		return ' '.join (Result)

	def check (self):
		return True


def main ():
	"Let's get dangerous!"
	from sys import stdin, exit
	from optparse import OptionParser
	Parser = OptionParser (prog='magenta', version='%prog ' + __version__)
	Parser.add_option ('-d', '--debug', action='store_true', default=False, help='print what would be done and do nothing')
	Options, Args = Parser.parse_args ()
	CurLine = 0
	Rules = []
	Xs = []
	while True:
		Line = stdin.readline ()
		if not Line:
			break
		Line = Line.strip ()
		CurLine += 1
		if Line and not Line.startswith ('#'):
			R = None
			try:
				R = Rule (Line)
			except RuleError as X:
				Xs.append ((CurLine, 'parse', X))
			else:
				try:
					R.check ()
				except RuleError as X:
					Xs.append ((CurLine, 'check', X))
			finally:
				Rules.append ((CurLine, R))
	print '\n'.join ("Line %i: %s error: %s" % (Line, Type, X) for Line, Type, X in Xs)
	print '\n'.join ("Line %i: %s" % (Line, R) for Line, R in Rules if R)


if __name__ == '__main__':
	main ()
